You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
654 lines
24 KiB
654 lines
24 KiB
<script setup lang="ts">
|
|
import { unwrapApiBody, type ApiResponse } from '../../utils/http/factory'
|
|
import { extractFrontMatterDesc, stripFrontMatter } from '../../utils/markdown-front-matter'
|
|
import { renderSafeMarkdown } from '../../utils/render-markdown'
|
|
import {
|
|
formatOccurredOnDisplay,
|
|
formatPublishedDateOnly,
|
|
occurredOnToIsoAttr,
|
|
} from '../../utils/timeline-datetime'
|
|
|
|
definePageMeta({
|
|
layout: 'public',
|
|
})
|
|
|
|
const route = useRoute()
|
|
const slug = computed(() => route.params.publicSlug as string)
|
|
const { mode } = usePublicProfileLayoutMode()
|
|
|
|
type ReadingSection = 'posts' | 'timeline' | 'rss'
|
|
const readingSection = ref<ReadingSection>('posts')
|
|
|
|
type PublicPostListItem = {
|
|
title: string
|
|
excerpt: string
|
|
slug: string
|
|
publishedAt: Date | null
|
|
coverUrl?: string | null
|
|
}
|
|
|
|
type PublicTimelineItem = {
|
|
id?: number
|
|
title?: string | null
|
|
occurredOn?: Date | string | null
|
|
linkUrl?: string | null
|
|
bodyMarkdown?: string | null
|
|
}
|
|
|
|
type PublicRssListItem = { title?: string | null; canonicalUrl?: string | null; canonical_url?: string | null }
|
|
|
|
type Payload = {
|
|
user: { publicSlug: string | null; nickname: string | null; avatar: string | null }
|
|
bio: { markdown: string } | null
|
|
links: { label: string; url: string; visibility: string }[]
|
|
posts: { items: PublicPostListItem[]; total: number }
|
|
timeline: { items: PublicTimelineItem[]; total: number }
|
|
rssItems: { items: PublicRssListItem[]; total: number }
|
|
}
|
|
|
|
function rssPublicHref(it: PublicRssListItem): string | undefined {
|
|
const u = it.canonicalUrl ?? it.canonical_url
|
|
return typeof u === 'string' && u.trim().length ? u : undefined
|
|
}
|
|
|
|
function rssPublicTitle(it: PublicRssListItem): string {
|
|
const t = it.title
|
|
return typeof t === 'string' && t.trim().length ? t : '未命名'
|
|
}
|
|
|
|
function rssHostname(href: string | undefined): string {
|
|
if (!href) {
|
|
return ''
|
|
}
|
|
try {
|
|
return new URL(href).hostname
|
|
}
|
|
catch {
|
|
return href
|
|
}
|
|
}
|
|
|
|
const { data, pending, error } = await useAsyncData(
|
|
() => `public-profile-${slug.value}`,
|
|
async () => {
|
|
const res = await $fetch<ApiResponse<Payload>>(`/api/public/profile/${encodeURIComponent(slug.value)}`)
|
|
return unwrapApiBody(res)
|
|
},
|
|
{ watch: [slug] },
|
|
)
|
|
|
|
function firstReadingSection(d: Payload): ReadingSection {
|
|
if (d.posts.total > 0) {
|
|
return 'posts'
|
|
}
|
|
if (d.timeline.total > 0) {
|
|
return 'timeline'
|
|
}
|
|
return 'rss'
|
|
}
|
|
|
|
function readingSectionValid(d: Payload, s: ReadingSection): boolean {
|
|
if (s === 'posts') {
|
|
return d.posts.total > 0
|
|
}
|
|
if (s === 'timeline') {
|
|
return d.timeline.total > 0
|
|
}
|
|
return d.rssItems.total > 0
|
|
}
|
|
|
|
watch([data, mode], () => {
|
|
if (!data.value || mode.value !== 'detailed') {
|
|
return
|
|
}
|
|
const d = data.value
|
|
if (!d.posts.total && !d.timeline.total && !d.rssItems.total) {
|
|
return
|
|
}
|
|
if (!readingSectionValid(d, readingSection.value)) {
|
|
readingSection.value = firstReadingSection(d)
|
|
}
|
|
}, { immediate: true })
|
|
|
|
function selectReadingSection(s: ReadingSection) {
|
|
if (!data.value || !readingSectionValid(data.value, s)) {
|
|
return
|
|
}
|
|
readingSection.value = s
|
|
nextTick(() => {
|
|
document.getElementById('public-reading-main')?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
})
|
|
}
|
|
|
|
function timelineItemKey(e: PublicTimelineItem, i: number): string | number {
|
|
return e.id ?? i
|
|
}
|
|
|
|
/** 主页生平:有 front matter 的 desc 时只展示 desc;否则展示全文(兼容无 FM 的简介) */
|
|
const bioHtml = computed(() => {
|
|
const md = data.value?.bio?.markdown
|
|
if (!md?.trim()) {
|
|
return ''
|
|
}
|
|
const desc = extractFrontMatterDesc(md)
|
|
if (desc) {
|
|
return renderSafeMarkdown(desc)
|
|
}
|
|
return renderSafeMarkdown(md)
|
|
})
|
|
|
|
/** 「查看全文」:正文在去掉 FM 后仍有内容时才显示(避免仅有 desc 时链到空页) */
|
|
const showBioReadMore = computed(() => {
|
|
const md = data.value?.bio?.markdown
|
|
if (!md?.trim()) {
|
|
return false
|
|
}
|
|
return stripFrontMatter(md).trim().length > 0
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div v-if="pending" class="text-muted py-10">
|
|
<UContainer>加载中…</UContainer>
|
|
</div>
|
|
<UContainer v-else-if="error" class="py-10">
|
|
<UAlert color="error" title="无法加载主页" />
|
|
</UContainer>
|
|
|
|
<!-- 展示:居中窄栏,原版卡片 + 时间轴 -->
|
|
<UContainer
|
|
v-else-if="data && mode === 'showcase'"
|
|
class="py-10 space-y-10 max-w-2xl"
|
|
>
|
|
<div class="flex flex-col gap-2">
|
|
<div v-if="data.user.avatar" class="flex justify-center">
|
|
<img
|
|
:src="data.user.avatar"
|
|
alt=""
|
|
class="h-20 w-20 rounded-full object-cover border border-default"
|
|
>
|
|
</div>
|
|
<h1 class="text-2xl font-semibold text-center">
|
|
{{ data.user.nickname || data.user.publicSlug || slug }}
|
|
</h1>
|
|
</div>
|
|
|
|
<div v-if="data.bio?.markdown && bioHtml" class="space-y-3">
|
|
<div
|
|
class="prose prose-neutral dark:prose-invert max-w-none prose-img:rounded-lg"
|
|
v-html="bioHtml"
|
|
/>
|
|
<div v-if="showBioReadMore" class="not-prose">
|
|
<NuxtLink
|
|
:to="`/@${slug}/about`"
|
|
class="text-sm font-medium text-primary hover:underline"
|
|
>
|
|
查看全文
|
|
</NuxtLink>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="data.links.length" class="space-y-2">
|
|
<h2 class="text-lg font-medium">
|
|
链接
|
|
</h2>
|
|
<ul class="space-y-1">
|
|
<li v-for="(l, i) in data.links" :key="i">
|
|
<a
|
|
:href="l.url"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
class="text-primary hover:underline"
|
|
>{{ l.label }}</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div v-if="data.posts.total" class="space-y-2">
|
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
|
<h2 class="text-lg font-medium">
|
|
文章
|
|
</h2>
|
|
<UButton
|
|
v-if="data.posts.total > 5"
|
|
:to="`/@${slug}/posts`"
|
|
variant="outline"
|
|
size="xs"
|
|
icon="i-lucide-arrow-right"
|
|
trailing
|
|
>
|
|
查看全部(共 {{ data.posts.total }} 条)
|
|
</UButton>
|
|
</div>
|
|
<ul class="space-y-2">
|
|
<li
|
|
v-for="p in data.posts.items"
|
|
:key="p.slug"
|
|
class="border border-default rounded-lg overflow-hidden transition-colors hover:bg-elevated/50"
|
|
>
|
|
<NuxtLink
|
|
:to="`/@${slug}/posts/${encodeURIComponent(p.slug)}`"
|
|
class="group block p-3 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary rounded-lg"
|
|
>
|
|
<time
|
|
v-if="p.publishedAt"
|
|
class="block text-xs font-medium tabular-nums text-muted"
|
|
:datetime="occurredOnToIsoAttr(p.publishedAt)"
|
|
>{{ formatPublishedDateOnly(p.publishedAt) }}</time>
|
|
<div class="mt-1 font-medium text-highlighted">
|
|
{{ p.title }}
|
|
</div>
|
|
<div v-if="p.excerpt" class="text-sm text-muted mt-1">
|
|
{{ p.excerpt }}
|
|
</div>
|
|
<div class="mt-1.5 text-sm font-medium text-primary group-hover:underline">
|
|
查看全文
|
|
</div>
|
|
</NuxtLink>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div v-if="data.timeline.total" class="space-y-3">
|
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
|
<h2 class="text-lg font-medium text-highlighted">
|
|
时光机
|
|
</h2>
|
|
<UButton
|
|
v-if="data.timeline.total > 5"
|
|
:to="`/@${slug}/timeline`"
|
|
variant="outline"
|
|
size="xs"
|
|
icon="i-lucide-arrow-right"
|
|
trailing
|
|
>
|
|
查看全部(共 {{ data.timeline.total }} 条)
|
|
</UButton>
|
|
</div>
|
|
<ul class="relative space-y-0">
|
|
<li
|
|
v-for="(e, i) in data.timeline.items"
|
|
:key="timelineItemKey(e, i)"
|
|
class="relative flex gap-4 pb-6 pl-1 last:pb-0"
|
|
>
|
|
<div
|
|
v-if="i < data.timeline.items.length - 1"
|
|
class="absolute left-[11px] top-5 bottom-0 w-px bg-default"
|
|
aria-hidden="true"
|
|
/>
|
|
<div class="relative z-[1] flex shrink-0 flex-col items-center pt-0.5">
|
|
<span class="size-2.5 rounded-full bg-primary ring-4 ring-primary/15" />
|
|
</div>
|
|
<article
|
|
class="min-w-0 flex-1 rounded-lg border border-default bg-elevated/40 px-3 py-3 shadow-sm"
|
|
>
|
|
<time
|
|
class="block text-xs font-medium tabular-nums text-muted"
|
|
:datetime="e.occurredOn ? occurredOnToIsoAttr(e.occurredOn) : undefined"
|
|
>{{ formatOccurredOnDisplay(e.occurredOn ?? '') }}</time>
|
|
<div class="mt-1.5 font-medium text-highlighted">
|
|
{{ e.title }}
|
|
</div>
|
|
<a
|
|
v-if="e.linkUrl"
|
|
:href="e.linkUrl"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
class="mt-2 inline-block text-sm text-primary hover:underline"
|
|
>相关链接</a>
|
|
</article>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div v-if="data.rssItems.total" class="space-y-2">
|
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
|
<h2 class="text-lg font-medium">
|
|
阅读
|
|
</h2>
|
|
<UButton
|
|
v-if="data.rssItems.total > 5"
|
|
:to="`/@${slug}/reading`"
|
|
variant="outline"
|
|
size="xs"
|
|
icon="i-lucide-arrow-right"
|
|
trailing
|
|
>
|
|
查看全部(共 {{ data.rssItems.total }} 条)
|
|
</UButton>
|
|
</div>
|
|
<ul class="space-y-2">
|
|
<li
|
|
v-for="(it, i) in data.rssItems.items"
|
|
:key="i"
|
|
class="rounded-lg border border-default transition-colors hover:bg-elevated/40"
|
|
>
|
|
<a
|
|
v-if="rssPublicHref(it)"
|
|
:href="rssPublicHref(it)"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
class="block p-3 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary rounded-lg"
|
|
>
|
|
<div class="font-medium text-primary">
|
|
{{ rssPublicTitle(it) }}
|
|
</div>
|
|
<div class="mt-1 text-sm text-muted break-all">
|
|
{{ rssHostname(rssPublicHref(it)) }}
|
|
</div>
|
|
</a>
|
|
<div v-else class="p-3 text-muted">
|
|
{{ rssPublicTitle(it) }}
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<UEmpty
|
|
v-if="!data.posts.total && !data.timeline.total && !data.rssItems.total && !data.bio && !data.links.length"
|
|
title="这里还没有公开内容"
|
|
description="站主尚未发布任何公开文章或动态。"
|
|
/>
|
|
</UContainer>
|
|
|
|
<!-- 阅读:侧栏 + 与展示一致的预览块 -->
|
|
<UContainer
|
|
v-else-if="data && mode === 'detailed'"
|
|
class="py-8 lg:py-10 max-w-6xl"
|
|
>
|
|
<div class="lg:grid lg:grid-cols-[12.5rem_minmax(0,1fr)] lg:gap-10 xl:grid-cols-[14rem_minmax(0,1fr)] xl:gap-12">
|
|
<aside class="mb-10 lg:mb-0">
|
|
<div class="lg:sticky lg:top-20 space-y-6">
|
|
<div class="flex flex-col gap-3 border-b border-default pb-6 lg:border-0 lg:pb-0">
|
|
<div v-if="data.user.avatar" class="flex justify-center lg:justify-start">
|
|
<img
|
|
:src="data.user.avatar"
|
|
alt=""
|
|
class="h-16 w-16 rounded-full object-cover border border-default lg:h-14 lg:w-14"
|
|
>
|
|
</div>
|
|
<h1 class="text-center text-lg font-semibold text-highlighted lg:text-left lg:text-base">
|
|
{{ data.user.nickname || data.user.publicSlug || slug }}
|
|
</h1>
|
|
<div v-if="data.bio?.markdown && bioHtml" class="space-y-2">
|
|
<div
|
|
class="bio-preview-scroll max-h-36 overflow-y-auto rounded-lg border border-default bg-elevated/30 p-3 text-xs leading-relaxed text-muted prose prose-neutral dark:prose-invert max-w-none prose-p:my-1 prose-img:rounded"
|
|
v-html="bioHtml"
|
|
/>
|
|
<NuxtLink
|
|
v-if="showBioReadMore"
|
|
:to="`/@${slug}/about`"
|
|
class="text-xs font-medium text-primary hover:underline"
|
|
>
|
|
查看全文
|
|
</NuxtLink>
|
|
</div>
|
|
<div v-if="data.links.length" class="space-y-1.5">
|
|
<div class="text-xs font-medium uppercase tracking-wide text-muted">
|
|
链接
|
|
</div>
|
|
<ul class="space-y-1 text-sm">
|
|
<li v-for="(l, i) in data.links" :key="i">
|
|
<a
|
|
:href="l.url"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
class="text-primary hover:underline break-all"
|
|
>{{ l.label }}</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<nav
|
|
class="flex flex-wrap gap-2 border-t border-default pt-4 lg:hidden"
|
|
aria-label="页面区块"
|
|
>
|
|
<button
|
|
v-if="data.posts.total"
|
|
type="button"
|
|
class="rounded-full border px-3 py-1 text-xs transition-colors"
|
|
:class="readingSection === 'posts' ? 'border-primary/40 bg-primary/10 text-highlighted' : 'border-default bg-elevated/40 text-muted hover:text-default'"
|
|
@click="selectReadingSection('posts')"
|
|
>
|
|
文章 · {{ data.posts.total }}
|
|
</button>
|
|
<button
|
|
v-if="data.timeline.total"
|
|
type="button"
|
|
class="rounded-full border px-3 py-1 text-xs transition-colors"
|
|
:class="readingSection === 'timeline' ? 'border-primary/40 bg-primary/10 text-highlighted' : 'border-default bg-elevated/40 text-muted hover:text-default'"
|
|
@click="selectReadingSection('timeline')"
|
|
>
|
|
时光机 · {{ data.timeline.total }}
|
|
</button>
|
|
<button
|
|
v-if="data.rssItems.total"
|
|
type="button"
|
|
class="rounded-full border px-3 py-1 text-xs transition-colors"
|
|
:class="readingSection === 'rss' ? 'border-primary/40 bg-primary/10 text-highlighted' : 'border-default bg-elevated/40 text-muted hover:text-default'"
|
|
@click="selectReadingSection('rss')"
|
|
>
|
|
阅读 · {{ data.rssItems.total }}
|
|
</button>
|
|
</nav>
|
|
<nav class="hidden lg:block space-y-1 border-t border-default pt-4 text-sm" aria-label="页面区块">
|
|
<button
|
|
v-if="data.posts.total"
|
|
type="button"
|
|
class="flex w-full items-center justify-between rounded-md px-2 py-2 text-left transition-colors"
|
|
:class="readingSection === 'posts' ? 'bg-elevated text-highlighted' : 'text-muted hover:bg-elevated/60 hover:text-default'"
|
|
@click="selectReadingSection('posts')"
|
|
>
|
|
<span>文章</span>
|
|
<span class="tabular-nums text-xs opacity-80">{{ data.posts.total }}</span>
|
|
</button>
|
|
<button
|
|
v-if="data.timeline.total"
|
|
type="button"
|
|
class="flex w-full items-center justify-between rounded-md px-2 py-2 text-left transition-colors"
|
|
:class="readingSection === 'timeline' ? 'bg-elevated text-highlighted' : 'text-muted hover:bg-elevated/60 hover:text-default'"
|
|
@click="selectReadingSection('timeline')"
|
|
>
|
|
<span>时光机</span>
|
|
<span class="tabular-nums text-xs opacity-80">{{ data.timeline.total }}</span>
|
|
</button>
|
|
<button
|
|
v-if="data.rssItems.total"
|
|
type="button"
|
|
class="flex w-full items-center justify-between rounded-md px-2 py-2 text-left transition-colors"
|
|
:class="readingSection === 'rss' ? 'bg-elevated text-highlighted' : 'text-muted hover:bg-elevated/60 hover:text-default'"
|
|
@click="selectReadingSection('rss')"
|
|
>
|
|
<span>阅读</span>
|
|
<span class="tabular-nums text-xs opacity-80">{{ data.rssItems.total }}</span>
|
|
</button>
|
|
</nav>
|
|
</div>
|
|
</aside>
|
|
|
|
<div id="public-reading-main" class="min-w-0 scroll-mt-24 space-y-14">
|
|
<section
|
|
v-show="readingSection === 'posts' && data.posts.total"
|
|
id="public-reading-posts"
|
|
>
|
|
<div class="flex flex-wrap items-center justify-between gap-2 mb-4">
|
|
<h2 class="text-xs font-semibold uppercase tracking-wider text-muted">
|
|
文章
|
|
</h2>
|
|
<UButton
|
|
v-if="data.posts.total > 5"
|
|
:to="`/@${slug}/posts`"
|
|
variant="outline"
|
|
size="xs"
|
|
icon="i-lucide-arrow-right"
|
|
trailing
|
|
>
|
|
查看全部(共 {{ data.posts.total }} 条)
|
|
</UButton>
|
|
</div>
|
|
<ul class="border-t border-default">
|
|
<li
|
|
v-for="p in data.posts.items"
|
|
:key="p.slug"
|
|
class="border-b border-default last:border-b-0"
|
|
>
|
|
<NuxtLink
|
|
:to="`/@${slug}/posts/${encodeURIComponent(p.slug)}`"
|
|
class="group flex flex-col gap-4 py-6 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-default rounded-lg sm:flex-row sm:gap-5"
|
|
>
|
|
<div
|
|
v-if="p.coverUrl"
|
|
class="w-full shrink-0 overflow-hidden rounded-xl border border-default sm:w-40"
|
|
>
|
|
<img
|
|
:src="p.coverUrl"
|
|
alt=""
|
|
class="aspect-[16/9] w-full object-cover sm:aspect-[4/3] sm:min-h-[7rem] sm:h-full"
|
|
>
|
|
</div>
|
|
<div class="min-w-0 flex-1">
|
|
<time
|
|
v-if="p.publishedAt"
|
|
class="block text-xs font-medium tabular-nums text-muted"
|
|
:datetime="occurredOnToIsoAttr(p.publishedAt)"
|
|
>{{ formatPublishedDateOnly(p.publishedAt) }}</time>
|
|
<div class="mt-1 text-xl font-semibold text-pretty text-highlighted leading-snug group-hover:text-primary transition-colors">
|
|
{{ p.title }}
|
|
</div>
|
|
<p
|
|
v-if="p.excerpt"
|
|
class="mt-3 text-sm text-muted text-pretty leading-relaxed"
|
|
>
|
|
{{ p.excerpt }}
|
|
</p>
|
|
<p class="mt-2 text-sm font-medium text-primary group-hover:underline">
|
|
查看全文
|
|
</p>
|
|
</div>
|
|
</NuxtLink>
|
|
</li>
|
|
</ul>
|
|
</section>
|
|
|
|
<section
|
|
v-show="readingSection === 'timeline' && data.timeline.total"
|
|
id="public-reading-timeline"
|
|
>
|
|
<div class="flex flex-wrap items-center justify-between gap-2 mb-4">
|
|
<h2 class="text-xs font-semibold uppercase tracking-wider text-muted">
|
|
时光机
|
|
</h2>
|
|
<UButton
|
|
v-if="data.timeline.total > 5"
|
|
:to="`/@${slug}/timeline`"
|
|
variant="outline"
|
|
size="xs"
|
|
icon="i-lucide-arrow-right"
|
|
trailing
|
|
>
|
|
查看全部(共 {{ data.timeline.total }} 条)
|
|
</UButton>
|
|
</div>
|
|
<ul class="relative space-y-0">
|
|
<li
|
|
v-for="(e, i) in data.timeline.items"
|
|
:key="timelineItemKey(e, i)"
|
|
class="relative flex gap-4 pb-6 pl-1 last:pb-0"
|
|
>
|
|
<div
|
|
v-if="i < data.timeline.items.length - 1"
|
|
class="absolute left-[11px] top-5 bottom-0 w-px bg-default"
|
|
aria-hidden="true"
|
|
/>
|
|
<div class="relative z-[1] flex shrink-0 flex-col items-center pt-0.5">
|
|
<span class="size-2.5 rounded-full bg-primary ring-4 ring-primary/15" />
|
|
</div>
|
|
<article
|
|
class="min-w-0 flex-1 rounded-xl border border-default bg-elevated/25 px-5 py-5 shadow-sm sm:px-6"
|
|
>
|
|
<div class="flex flex-col gap-1 sm:flex-row sm:items-baseline sm:justify-between sm:gap-4">
|
|
<time
|
|
class="shrink-0 text-xs font-medium tabular-nums text-muted sm:order-2 sm:text-right"
|
|
:datetime="e.occurredOn ? occurredOnToIsoAttr(e.occurredOn) : undefined"
|
|
>{{ formatOccurredOnDisplay(e.occurredOn ?? '') }}</time>
|
|
<h3 class="text-pretty text-lg font-semibold text-highlighted sm:order-1 sm:min-w-0 sm:flex-1">
|
|
{{ e.title }}
|
|
</h3>
|
|
</div>
|
|
<p
|
|
v-if="e.bodyMarkdown && e.bodyMarkdown.trim()"
|
|
class="mt-4 whitespace-pre-wrap border-t border-default/60 pt-4 text-sm leading-relaxed text-default text-pretty"
|
|
>
|
|
{{ e.bodyMarkdown }}
|
|
</p>
|
|
<a
|
|
v-if="e.linkUrl"
|
|
:href="e.linkUrl"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
class="mt-4 inline-flex items-center gap-1.5 text-sm font-medium text-primary hover:underline"
|
|
>
|
|
<span>打开链接</span>
|
|
<UIcon name="i-lucide-external-link" class="size-3.5 opacity-80" />
|
|
</a>
|
|
</article>
|
|
</li>
|
|
</ul>
|
|
</section>
|
|
|
|
<section
|
|
v-show="readingSection === 'rss' && data.rssItems.total"
|
|
id="public-reading-rss"
|
|
>
|
|
<div class="flex flex-wrap items-center justify-between gap-2 mb-4">
|
|
<h2 class="text-xs font-semibold uppercase tracking-wider text-muted">
|
|
阅读
|
|
</h2>
|
|
<UButton
|
|
v-if="data.rssItems.total > 5"
|
|
:to="`/@${slug}/reading`"
|
|
variant="outline"
|
|
size="xs"
|
|
icon="i-lucide-arrow-right"
|
|
trailing
|
|
>
|
|
查看全部(共 {{ data.rssItems.total }} 条)
|
|
</UButton>
|
|
</div>
|
|
<ul class="border-t border-default">
|
|
<li
|
|
v-for="(it, i) in data.rssItems.items"
|
|
:key="i"
|
|
class="border-b border-default last:border-b-0"
|
|
>
|
|
<a
|
|
v-if="rssPublicHref(it)"
|
|
:href="rssPublicHref(it)"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
class="group block py-5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary rounded-lg"
|
|
>
|
|
<div class="text-base font-semibold text-pretty text-primary group-hover:underline">
|
|
{{ rssPublicTitle(it) }}
|
|
</div>
|
|
<div class="mt-1.5 break-all text-sm text-muted">
|
|
{{ rssHostname(rssPublicHref(it)) }}
|
|
</div>
|
|
</a>
|
|
<div v-else class="py-5 text-muted">
|
|
{{ rssPublicTitle(it) }}
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
</section>
|
|
|
|
<UEmpty
|
|
v-if="!data.posts.total && !data.timeline.total && !data.rssItems.total && !data.bio && !data.links.length"
|
|
title="这里还没有公开内容"
|
|
description="站主尚未发布任何公开文章或动态。"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</UContainer>
|
|
</template>
|
|
|